package org.bhf.json;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

/**
 * A JSONTokenizer takes a source string and extracts characters and tokens from
 * it. You should not need to user this class: it is artificially public.
 * @exclude
 */
public class JSONTokenizer
{
    /** The index of the next character. */
    private int index;

    /** The source string being tokenized. */
    private String json;

    /**
     * Construct a JSONTokenizer from a string.
     *
     * @param source A source string. Must not be <code>null</code>
     */
    public JSONTokenizer( final String source )
    {
        this.index = 0;
        this.json = source;
    }

    /**
     * Return an information string to help diagnose syntax errors.
     * @return An information string to help diagnose syntax errors.
     */
    public String   getDiagnosticContext()
    {
        final int startOfContext = Math.max( index - 10, 0 );
        final int endOfContext = Math.min( index + 10, json.length() );
        final String context = json.substring( startOfContext, endOfContext );

        if( !hasMore() )
            return "Unexpected end of JSON: ..." + context;
        return "Syntax error at position " + (index - startOfContext) + " in ..." + context + "...";
    }

    /**
     * Back up one character. This provides a sort of lookahead capability, so
     * that you can test for a digit or letter before attempting to parse the
     * next number or identifier.
     */
    public void back()
    {
        if( index > 0 ) index--;
    }

    /**
     * Determine if the source string still contains characters that next() can
     * consume.
     *
     * @return <code>true</code> if not yet at the end of the source.
     */
    public final boolean hasMore()
    {
        return index < json.length();
    }

    /**
     * Get the next character in the source string.
     *
     * @return The next character, or 0 if past the end of the source string.
     */
    public final char next()
    {
        return hasMore() ? json.charAt( index++ ) : 0;
    }

    /**
     * Look at the next character in the source string.
     *
     * @return The next character, or 0 if past the end of the source string.
     */
    public char peek()
    {
        return hasMore() ? json.charAt( index ) : 0;
    }

    /**
     * Get the next count characters.
     *
     * @param count The number of characters to take.
     * @return A string of count characters.
     * @throws SyntaxError Substring bounds error if there are not count characters
     *                       remaining in the source string.
     */
    public String next( final int count )
    {
        final int start = index;
        int stop = start + count;
        if( stop >= this.json.length() )
        {
            throw new SyntaxError( "Substring bounds error.", this );
        }
        index = stop;

        return json.substring( start, stop );
    }

    /**
     * Get the next char in the string, skipping whitespace and comments
     * (slashslash, slashstar, and hash).
     *
     * @return A character, or 0 if there are no hasMore characters.
     * @throws SyntaxError
     */
    public char nextClean()
    {
        while( true )
        {
            char c = next();
            if( c == '/' )
            {
                switch( next() )
                {
                    case'/':
                        do
                        {
                            c = next();
                        }
                        while( c != '\n' && c != '\r' && c != 0 );
                        break;
                    case'*':
                        for( ; ; )
                        {
                            c = next();
                            if( c == 0 )
                            {
                                throw new SyntaxError( "Unclosed comment.", this );
                            }
                            if( c == '*' )
                            {
                                if( next() == '/' )
                                {
                                    break;
                                }
                                back();
                            }
                        }
                        break;
                    default:
                        back();
                        return '/';
                }
            }
            else if( c == '#' )
            {
                do
                {
                    c = next();
                }
                while( c != '\n' && c != '\r' && c != 0 );
            }
            else if( !Character.isWhitespace( c ) || c == 0 )
            {
                return c;
            }
        }
    }

    /**
     * Return the characters up to the next close quote character. Backslash
     * processing is done. The formal JSON format does not allow strings in
     * single quotes, but an implementation is allowed to accept them.
     *
     * @param quote The quoting character, either <code>"</code>&nbsp;<small>(double
     *              quote)</small> or <code>'</code>&nbsp;<small>(single quote)</small>.
     * @return A String.
     * @throws SyntaxError Unterminated string.
     */
    public String nextQuotedString( final char quote )
    {
        char c;
        StringBuffer sb = new StringBuffer();
        while( true )
        {
            c = next();
            switch( c )
            {
                case 0:
                case'\n':
                case'\r':
                    throw new SyntaxError( "Unterminated string: ", this );
                case'\\':
                    c = next();
                    switch( c )
                    {
                        case'b':
                            sb.append( '\b' );
                            break;
                        case't':
                            sb.append( '\t' );
                            break;
                        case'n':
                            sb.append( '\n' );
                            break;
                        case'f':
                            sb.append( '\f' );
                            break;
                        case'r':
                            sb.append( '\r' );
                            break;
                        case'u':
                            sb.append( (char)Integer.parseInt( next( 4 ), 16 ) );
                            break;
                        case'x':
                            sb.append( (char)Integer.parseInt( next( 2 ), 16 ) );
                            break;
                        default:
                            sb.append( c );
                    }
                    break;
                default:
                    if( c == quote )
                    {
                        return sb.toString();
                    }
                    sb.append( c );
            }
        }
    }

    /**
     * Return the next value string: null, true, false, or a number.
     *
     * @return A String.
     */
    public String nextUnQuotedString()
    {
        final StringBuffer sb = new StringBuffer();
        for( char c = next(); c != 0 && !Character.isWhitespace( c ) && ",:]}/\\\"[{;=#".indexOf( c ) < 0; c = next() )
        {
            sb.append( c );
        }
        back();
        return sb.toString().trim();
    }

    /**
     * Skip whitespace and eturn the next value of any kind: quotes stripped and escapes converted.
     *
     * @return A String.
     */
    public String nextAnyValueString()
    {
        if( !hasMore() )
            throw new SyntaxError( "", this );

        final char c = nextClean();
        back();
        switch( c )
        {
            case '\'':
            case '"':
                return nextQuotedString( next() );
            default:
                final String value = nextUnQuotedString();

                return "null".equals( value ) ? null : value;
        }
    }

    /**
     * Make a printable string of this JSONTokenizer.
     *
     * @return " at character [this.index] of [this.json]"
     */
    public String toString()
    {
        return " at character " + index + " of " + json;
    }
}